기계음·영어 발화 감지 로직 분석
apps/web — use-ai-audio-noise-guard.ts / use-ai-english-guard.ts / use-monitor-session.ts ·
2026-05-20
1. 전체 감지 시스템 개요
PPI는 AI(핑퐁이)가 비정상 오디오를 출력할 때 이를 감지하는 두 개의 독립적인 방어 시스템을 운영합니다.
🔊 기계음 감지 Noise Guard
AI 오디오 스트림의 진폭 변동계수(CV)와 영교차율(ZCR)을 분석하여 기계적으로 균일한 파형을 감지합니다.
파일: hooks/use-ai-audio-noise-guard.ts
🗣️ 영어 발화 감지 MFCC Guard
AI 오디오 스트림에서 MFCC 특징을 추출해 사전 녹음된 영어 발음 템플릿과 코사인 유사도를 비교합니다.
파일: hooks/use-ai-english-guard.ts
2. 엔드-투-엔드 데이터 흐름
Guest (아동 브라우저)
AI 오디오 스트림 수신
OpenAI Realtime API → aiAudioStream (MediaStream)
↓
기계음 감지 (병렬 실행)
useAiAudioNoiseGuard
50ms 주기 AnalyserNode 분석 · CV + ZCR 판정 · 2-stage 재검증
+
영어 발화 감지 (병렬 실행)
useAiEnglishGuard
ScriptProcessorNode → MFCC 추출 → 코사인 유사도 → 패턴 시퀀스 매칭
↓ 감지 시
Guest Action (기계음)
cancelResponse() 호출 + sendIssueReport()
소켓 이벤트: GUEST_ISSUE_REPORT { issueText: "AI 오디오 기계음 감지 - 자동 중단", icon: "🔊" }
|
Guest Action (영어)
cancelResponse() 호출 + emitMfccEventToMonitor()
소켓 이벤트: GUEST_MFCC_LOG_EVENT { event: { type: "english_detected", sim, phrase, consecutive } }
↓ Socket.io 전달
Socket Server
GROUP_ALERT_BROADCAST 릴레이
같은 그룹 내 다른 세션 모니터들에게도 이벤트 브로드캐스트
↓
Monitor (치료사 브라우저) — use-monitor-session.ts
handleGuestIssueReport / handleMfccLogEvent
이벤트 수신 → AlertType 매핑 → setAlerts + useMonitorDataStore.addAlert
↓ 사이드이펙트
모니터 UI
Alert 패널 + Toast 알림
issueToast (3s 자동 닫힘) · alerts[] 배열 추가
+
MFCC 로그 (영어 감지 시)
turn_end 수신 → S3 업로드
JSONL blob → /api/session-logs/{turnId}/mfcc-upload-url → S3 PUT → PATCH mfccS3Key
3. 기계음 감지 알고리즘 상세
파일: apps/web/hooks/use-ai-audio-noise-guard.ts
3-1. 핵심 파라미터
| 상수 | 값 | 설명 |
| ANALYSIS_INTERVAL_MS | 50 | 분석 주기 (ms) |
| RING_BUFFER_SIZE | 14 | 1차 판정 링 버퍼 크기 (14프레임 = 700ms) |
| NOISE_FRAME_THRESHOLD | 10 | 링 버퍼 중 기계음 판정 필요 프레임 수 (≈71%) |
| CV_THRESHOLD | 0.35 | RMS 변동계수 임계값 (이하 → 기계음 의심) |
| ZCR_MIN_THRESHOLD | 0.04 | 영교차율 최소값 (이 이상이어야 기계음 판정 유효) |
| RMS_SILENCE_FLOOR_DB | –50 dB | 무음 필터 기준 (이하는 판정 제외) |
| GRACE_PERIOD_MS | 500 | AI 발화 시작 후 판정 유예 구간 |
| REVERIFY_DURATION_MS | 200 | 1차 감지 후 2차 재검증 기간 |
| REVERIFY_FRAMES | 4 | 재검증 프레임 수 |
| REVERIFY_THRESHOLD | 3 | 재검증 통과 필요 기계음 프레임 수 |
| COOLDOWN_MS | 10,000 | 연속 감지 억제 쿨다운 (ms) |
| RMS_WINDOW_SIZE | 8 | CV 계산용 RMS 슬라이딩 윈도우 크기 (400ms) |
| FFT_SIZE | 2048 | AnalyserNode FFT 크기 |
3-2. 판정 파이프라인
무음 필터
50ms 마다 AnalyserNode에서 Float32Array 읽음. RMS가 –50 dB 미만이면 해당 프레임 건너뜀.
RMS 슬라이딩 윈도우 (8프레임)
최근 8프레임(400ms)의 dB RMS를 linear 변환 후 배열에 유지. CV 계산에 사용.
CV 계산 (변동계수)
CV = std(linear_rms) / mean(linear_rms)
정상 음성 CV ≈ 0.85 · 기계음 CV ≈ 0.27. 임계값 0.35 이하이면 기계음 후보.
ZCR 보조 조건
영교차율이 0.04 이상이어야 판정 유효. 너무 매끈한 저주파 구간의 오탐을 보완.
1차 판정 — 링 버퍼
14프레임 원형 버퍼에 기계음 여부 Boolean 기록. 10/14 이상이면 후보 감지.
2차 재검증 (200ms / 4프레임)
후보 감지 후 200ms 추가 관찰. 4프레임 중 3프레임 이상 기계음이면 확정 감지. 아니면 링 버퍼 초기화 후 재시작.
쿨다운 체크 (10초)
마지막 감지로부터 10초 이내면 콜백 호출 억제.
onNoiseDetected() 콜백
cancelResponse() 호출 → AI 응답 즉시 중단.
guestSocket.sendIssueReport("AI 오디오 기계음 감지 - 자동 중단", "🔊") 전송.
설계 의도: 리뷰 반영으로 1차 감지 즉시 중단 대신 200ms 재검증을 추가. 짧은 무음 dip이나 발화 초기 과도기(500ms 유예)에서 발생하는 오탐을 줄이기 위한 구조.
4. 영어 발화 감지 알고리즘 상세 (MFCC)
파일: apps/web/hooks/use-ai-english-guard.ts + apps/web/lib/mfcc.ts
4-1. MFCC 추출 파라미터
| 파라미터 | 값 | 설명 |
| SAMPLE_RATE | 16,000 Hz | 처리 샘플레이트 (입력을 여기로 다운샘플) |
| FRAME_SIZE | 400 samples | 25ms @ 16kHz |
| HOP_SIZE | 160 samples | 10ms 스텝 |
| FFT_SIZE | 512 | Radix-2 Cooley-Tukey FFT |
| NUM_MEL_FILTERS | 26 | Mel 필터뱅크 수 |
| NUM_MFCC | 13 | MFCC 계수 수 (c0~c12) |
| PRE_EMPHASIS | 0.97 | 고주파 강조 계수 |
4-2. 슬라이딩 윈도우 설정
| 상수 | 값 | 설명 |
| WINDOW_SAMPLES_16K | 4,800 | 300ms @ 16kHz 분석 윈도우 |
| HOP_SAMPLES_16K | 2,400 | 150ms 슬라이딩 간격 |
| SCRIPT_PROCESSOR_BUFFER_SIZE | 4,096 | ScriptProcessorNode 버퍼 |
| MIN_RMS_ENERGY | 0.005 | 무음 구간 스킵 기준 |
| GRACE_PERIOD_MS | 500 | AI 발화 시작 후 유예 구간 |
| COOLDOWN_MS | 10,000 | 재감지 억제 쿨다운 |
| TOP_N_TEMPLATES | 3 | 검증 로그에 포함할 상위 유사도 템플릿 수 |
| MAX_SILENCE_HOPS_FOR_RESET | 5 | 연속 무음 hop 한계 (≈750ms 까지 커서 유지) |
| PATTERN_RULE_MISS_TOLERANCE | 2 | 패턴 시퀀스 miss 허용 횟수 |
4-3. 감지 알고리즘 — 두 가지 모드
모드 A: 패턴 시퀀스 매칭 주 감지 방식
mfccPatternRules 존재 시 활성화
서버에서 내려온 MfccPatternRule[]로 순서 있는 템플릿 시퀀스 정의
각 rule에 cursor 유지
ruleCursorsRef: Map<ruleId, cursor> — 현재 몇 번째 스텝 대기 중인지 추적
스텝 매칭 조건
maxSimilarity ≥ 전역 threshold AND bestTemplateIdx === 기대 tldx AND sim ≥ 스텝별 threshold
miss 허용 (PATTERN_RULE_MISS_TOLERANCE = 2)
2회 이상 miss 시 cursor 리셋. 음절 경계 dip에서 시퀀스가 중단되지 않도록 보완.
시퀀스 완성 + stopSpeech=true
onEnglishDetected(tldxSequence) 호출 → AI 응답 즉시 중단
모드 B: Consecutive 매칭 로그 전용
패턴 규칙 없을 때 fallback
연속 매칭 카운터(consecutiveMatchCount)로 판단
같은 phraseGroup 내에서만 누적
그룹이 바뀌거나 offset이 감소하면 카운터 리셋
threshold 도달 시 로그만 남김
※ 현재는 action 없음 — 패턴 시퀀스 감지가 주 트리거
4-4. MFCC 추출 파이프라인
PCM 입력 (임의 SR)
→
16kHz 다운샘플 (선형 보간)
→
Pre-emphasis (α=0.97)
→
Hamming 윈도우 (25ms)
→
FFT-512 (Radix-2)
→
파워 스펙트럼
→
Mel 필터뱅크 26개
→
log(E)
→
DCT-II → 13 계수
→
c0 제거 후 코사인 유사도
c0(에너지 계수) 제거 이유: MFCC의 첫 번째 계수(c0)는 모든 음성에서 비슷한 에너지 레벨을 가져 오탐을 유발합니다. c1~c12만 비교해 음색 유사도를 더 정확하게 측정합니다.
5. 모니터 측 MFCC 로그 수집 시스템
파일: entities/monitor-session/model/use-monitor-session.ts + hooks/use-mfcc-log-collector.ts
5-1. 턴 버퍼 구조
버퍼 관련 상수
| 상수 | 값 |
| MONITOR_MFCC_MAX_EVENTS_PER_TURN | 2,000 |
| MFCC_TURN_END_FLUSH_GRACE_MS | 1,500 ms |
| MFCC_TURN_END_FLUSH_INFLIGHT_POLL_MS | 200 ms |
| MFCC_TURN_END_FLUSH_MAX_WAIT_MS | 6,000 ms |
핵심 Ref 변수
| 변수 | 설명 |
| currentTurnBufferRef | 현재 턴 이벤트 버퍼 (MfccLogEvent[]) |
| inActiveTurnRef | ai_speaking_start ~ turn_end 구간 flag |
| currentTurnUserIdRef | 현재 턴 소유 userId |
| pendingTurnFlushRef | grace 지연 flush 타이머 정보 |
| mfccUploadInProgressRef | 중복 업로드 방지 Set<turnId> |
5-2. 이벤트 타입 분류 (MfccLogEvent)
| type | 설명 | 소비 방식 |
| session_start | 세션 시작 메타 정보 | 버퍼 append |
| session_end | 세션 종료 | 버퍼 append |
| ai_speaking_start | AI 발화 시작 → 새 턴 시작 | 이전 미완결 버퍼 flush + inActiveTurn=true |
| ai_speaking_stop | AI 발화 중단 | 버퍼 append |
| transcript | 발화 전사 스냅샷 | 버퍼 append |
| mfcc_sample | 300ms 윈도우 MFCC + 코사인 유사도 | 버퍼 append |
| match_candidate | 연속 매칭 후보 (로그용) | 버퍼 append |
| english_detected | 영어 발화 최종 감지 | 버퍼 append + onMfccEnglishDetected 콜백 |
| pattern_step | 패턴 시퀀스 진행 스텝 | 버퍼 append |
| reset | consecutive 카운터 리셋 이유 | 버퍼 append |
| turn_end | 턴 종료 → grace flush 스케줄 | grace 타이머 시작 후 S3 업로드 |
5-3. turn_end 처리 — Grace Flush 흐름
소켓 이벤트
turn_end 수신 (GUEST_MFCC_LOG_EVENT)
↓
처리 1
이전 pendingFlush 즉시 실행
이전 턴이 grace 중이었다면 clearTimeout 후 즉시 flushTurnMfcc 호출
→
처리 2
Grace 타이머 시작 (1,500ms)
inActiveTurn 유지 — 그 사이 도착하는 이벤트도 현재 버퍼에 수용
↓ 1,500ms 후 (또는 다음 ai_speaking_start 시 즉시)
Inflight 폴링
getInflightLidDetection() 체크
Whisper 추론이 in-flight면 200ms 간격으로 재폴링. 최대 6,000ms 하드 캡.
↓ inflight 완료 또는 하드 캡
S3 업로드
uploadTurnMfccLog
/api/session-logs/{turnId}/mfcc-upload-url → Presigned URL → S3 PUT (JSONL)
→
DB 연결
registerTurnMfccLog
/api/session-logs/{turnId}/mfcc PATCH — mfccS3Key, mfccRange 업데이트
5-4. mfccRange 요약 메타 (summarizeTurnMfcc)
| 필드 | 설명 |
| startMs / endMs | 턴 이벤트 타임스탬프 범위 |
| eventCount | 총 이벤트 수 |
| sampleCount | mfcc_sample 이벤트 수 |
| detectedEnglish | english_detected 이벤트 존재 여부 |
| detectedByMfcc | detectedEnglish와 동일 |
| maxSim | mfcc_sample 또는 english_detected 중 최대 코사인 유사도 |
| matchedPatternSequence | pattern_step에서 완성된 tldx 시퀀스 배열 |
6. 모니터 수신 처리 — Alert 생성
6-1. 이벤트 → AlertType 매핑
🔊
GUEST_ISSUE_REPORT → issueText.includes("기계음 감지")
AlertType.AI_MECHANICAL_NOISE · 우선순위 P0
즉시 토스트
🗣️
GUEST_ISSUE_REPORT → issueText.includes("영어 발화 감지")
AlertType.AI_ENGLISH_DETECTED · 우선순위 P0
즉시 토스트
🧩
GUEST_ISSUE_REPORT → issueText.includes("영어 발화 패턴 감지")
AlertType.MFCC_PATTERN_MATCHING · 우선순위 P0
즉시 토스트
🆘
GUEST_ISSUE_REPORT → 기타 issueText
AlertType.CHILD_HELP_REQUEST · 우선순위 P0
즉시 토스트
6-2. MFCC english_detected 이벤트 처리 흐름
handleMfccLogEvent 수신
roomId 필터링 후 event.type 분기
event.type === "ai_speaking_start"
inActiveTurn = true. 이전 pendingFlush가 있으면 즉시 실행.
event.type === "english_detected"
onMfccEnglishDetectedRef.current({ sim, phrase, consecutive }) 콜백 호출. 버퍼에도 append.
event.type === "turn_end"
Grace 타이머 스케줄. inActiveTurn = false 는 flush 완료 후 처리.
6-3. 중복 감지 방지 메커니즘
mountedMonitorRoomIds
카드뷰에서 같은 그룹의 여러 세션이 동시 마운트될 때 GROUP_ALERT_BROADCAST 중복 처리를 방지하는 모듈-레벨 Set.
원본 roomId를 담당하는 카드가 이미 직접 처리하므로 브로드캐스트 경로에서 skip.
mfccUploadInProgressRef
Set<turnId>로 동일 turnId에 대한 동시 S3 업로드를 방지. flushTurnMfcc 진입 시 체크하고, finally에서 삭제.
7. 연결 해제 시 버퍼 정리
guest-disconnected / peer-left 수신 시: pendingTurnFlushRef 타이머를 clearTimeout하고 currentTurnBufferRef를 비웁니다. turn_end 없이 끊어진 미완결 턴은 S3에 업로드하지 않고 폐기합니다 (튜닝 가치가 낮고, 다음 세션 시작 시 버퍼 오염 방지 목적).
컴포넌트 unmount 시: pendingTurnFlushRef 타이머만 clearTimeout합니다. 남은 버퍼는 폐기되며 S3 업로드 호출을 생략합니다.
8. 시스템 요약
| 항목 |
기계음 감지 (NoiseGuard) |
영어 발화 감지 (MFCC Guard) |
| 감지 원리 |
RMS CV < 0.35 + ZCR 조건 |
MFCC 코사인 유사도 + 패턴 시퀀스 |
| 분석 주기 |
50ms (AnalyserNode) |
150ms 슬라이딩 hop (ScriptProcessor) |
| 감지 확인 방식 |
2-stage (링 버퍼 + 재검증 200ms) |
패턴 시퀀스 완성 (miss 2회 허용) |
| AI 응답 중단 |
cancelResponse() 즉시 호출 |
onEnglishDetected → cancelResponse() |
| 소켓 이벤트 |
GUEST_ISSUE_REPORT |
GUEST_MFCC_LOG_EVENT |
| 모니터 AlertType |
AI_MECHANICAL_NOISE (P0) |
AI_ENGLISH_DETECTED / MFCC_PATTERN_MATCHING (P0) |
| 로그 저장 |
없음 |
JSONL → S3 (turn_end grace flush) |
| 쿨다운 |
10초 |
10초 |
| 발화 시작 유예 |
500ms |
500ms |
생성: 2026-05-20 · 분석 대상: apps/web (develop 브랜치) ·
주요 파일: use-ai-audio-noise-guard.ts, use-ai-english-guard.ts, use-monitor-session.ts, use-mfcc-log-collector.ts, lib/mfcc.ts